nanopyx.core.analysis.registration
1from functools import cache 2 3import numpy as np 4 5from skimage.filters import window 6 7from .estimate_shift import GetMaxOptimizer 8from .ccm_helper_functions import make_even_square 9from .ccm import calculate_slice_ccm 10from ..transform.interpolation_catmull_rom import Interpolator 11 12class Registration: 13 14 def __init__(self, image:np.ndarray, ref_image:np.ndarray): 15 """ 16 Register an image against a reference image 17 :param image: 2D array to register 18 :param ref_image: 2D array to use as reference 19 """ 20 21 assert image.ndim == 2, "Image must be 2D" 22 assert ref_image.ndim == 2, "Image must be 2D" 23 24 self.image = make_even_square(image[np.newaxis,:,:].astype(np.float32))[0,:,:] 25 self.ref_image = make_even_square(ref_image[np.newaxis,:,:].astype(np.float32))[0,:,:] 26 27 self.original_dtype = image.dtype 28 29 self.w = self.image.shape[1] 30 self.h = self.image.shape[0] 31 self.wref = self.ref_image.shape[1] 32 self.href = self.ref_image.shape[0] 33 34 self.reg_result = {} 35 36 37 def translation(self): 38 """ 39 Registers the images considering only translation 40 :return: registered image 41 """ 42 43 shifts, max_sim = self.phase_correlation(self.ref_image, self.image) 44 45 # The size of the CCM array is the same as the image 46 y_shift = (self.h/2.0 - shifts[0]) 47 x_shift = (self.w/2.0 - shifts[1]) 48 49 translated = Interpolator(self.image).shift(x_shift,y_shift).astype(self.original_dtype) 50 51 self.reg_result = {'Image':translated, 'Translation':(y_shift,x_shift), 'Scaling':None, 'Rotation':None, 'Max_Sim':max_sim} 52 53 return translated 54 55 56 def scaled_rotation(self): 57 """ 58 Registers the images considering only rotation and isotropic scaling 59 """ 60 61 lpolar_image = Interpolator(self.image).polar(scale='log') 62 lpolar_ref_image = Interpolator(self.ref_image).polar(scale='log') 63 shifts, max_sim = self.phase_correlation(lpolar_ref_image, lpolar_image) 64 65 # Size of the polar transform is always (360,maxradius) 66 h = 360 67 w = np.hypot(self.w/2, self.h/2) 68 69 angle = -np.deg2rad((h/2 - shifts[0])) 70 log_translation = (w/2-shifts[1]) * np.log(w) / w 71 scale = np.exp(log_translation) 72 73 scaled = Interpolator(self.image).scale_xy(scale, scale) 74 rotated = Interpolator(scaled).rotate(angle).astype(self.original_dtype) 75 76 self.reg_result = {'Image':rotated, 'Translation':None, 'Scaling':scaled, 'Rotation':angle, 'Max_Sim':max_sim} 77 78 return rotated 79 80 def scaling_rotation_translation(self): 81 """ 82 Registers the images considering rotation, isotropic scaling and translation 83 Based upon: 84 An FFT-Based Technique for Translation,Rotation, 85 and Scale-Invariant Image Registration 86 B. Srinivasa Reddy and B. N. Chatterji 87 """ 88 # Step 0: Prepare some heavily used vars 89 h = 360 90 w = np.hypot(self.w/2, self.h/2) 91 highpass_filter = self.highpass_filter((self.h,self.w)) 92 93 # Step 1: Prep the reference image for iteration 94 windowed_ref_image = self.ref_image * window('hann', self.ref_image.shape) 95 freq_ref_image = np.abs(np.fft.fftshift(np.fft.fft2(windowed_ref_image)) * highpass_filter).astype(np.float32) 96 lpolar_ref_image = Interpolator(freq_ref_image).polar('log') 97 98 # Step 2: Iterate to find scale and angle 99 total_angle = 0 100 total_scale = 1 101 iter_image = self.image 102 103 for iter in range(10): 104 windowed_image = iter_image * window('hann', iter_image.shape) 105 f_image = np.abs(np.fft.fftshift(np.fft.fft2(windowed_image))*highpass_filter).astype(np.float32) 106 lpolar_image = Interpolator(f_image).polar('log') 107 108 shifts, max_sim_1 = self.phase_correlation(lpolar_ref_image, lpolar_image) 109 110 angle = -np.deg2rad((h/2 - shifts[0])) 111 log_translation = (w/2-shifts[1]) * np.log(w) / w 112 scale = np.exp(-1*log_translation) # NEGATIVE SIGN HERE IS MANDATORY 113 114 total_angle += angle 115 total_scale *= scale 116 117 scaled = Interpolator(self.image).scale_xy(total_scale, total_scale) 118 rotated = Interpolator(scaled).rotate(total_angle) 119 iter_image = rotated 120 121 # Step 3: Find translation AND the correct angle 122 # As outlined in the ref, in the frequency space 180-angle or angle have the same effect so there is ambiguity 123 # We test for both options and choose the one with the highest peak in the ccm 124 125 final_scaled_img = Interpolator(self.image).scale_xy(total_scale, total_scale) 126 127 final_option_1 = Interpolator(final_scaled_img).rotate(total_angle) 128 final_option_2 = Interpolator(final_scaled_img).rotate(np.pi+total_angle) 129 130 shifts_1, sim_1 = self.phase_correlation(self.ref_image, final_option_1) 131 shifts_2, sim_2 = self.phase_correlation(self.ref_image, final_option_2) 132 133 if sim_1 > sim_2: 134 final_iter_image = final_option_1 135 shifts = shifts_1 136 max_sim_2 = sim_1 137 else: 138 final_iter_image = final_option_2 139 shifts = shifts_2 140 max_sim_2 = sim_2 141 142 y_shift = (self.h/2.0 - shifts[0]) 143 x_shift = (self.w/2.0 - shifts[1]) 144 145 translated = Interpolator(final_iter_image).shift(x_shift,y_shift).astype(self.original_dtype) 146 147 self.reg_result = {'Image':translated, 'Translation':(y_shift,x_shift), 'Scaling':total_scale, 'Rotation':total_angle, 'Max_Sim':(max_sim_1,max_sim_2)} 148 149 return translated 150 151 @staticmethod 152 def phase_correlation(im1:np.ndarray, im2:np.ndarray)->tuple: 153 """ 154 Perform phase correlation between two images and return the shift that maximizes the overlap between the images 155 :param im1: 2D array of np.float32 156 :param im2: 2D array of np.float32 157 :return: coordinate tuple of the maximum point of the ccm and max value of the ccm 158 """ 159 160 ccm = calculate_slice_ccm(im1, im2) 161 optimizer = GetMaxOptimizer(ccm) 162 shifts = optimizer.get_max() 163 maxsim = -optimizer.get_interpolated_px_value(shifts) 164 165 return shifts, maxsim 166 167 @staticmethod 168 @cache 169 def highpass_filter(shape)->np.ndarray: 170 171 n_row = shape[0] 172 n_col = shape[1] 173 row_freq_arr = np.fft.fftshift(np.fft.fftfreq(n_row)) 174 col_freq_arr = np.fft.fftshift(np.fft.fftfreq(n_col)) 175 row_f,col_f = np.meshgrid(row_freq_arr, col_freq_arr, indexing='ij') 176 X = np.cos(np.pi*row_f) * np.cos(np.pi*col_f) 177 H = (1-X)*(2-X) 178 179 return H.astype(np.float32)
class
Registration:
14class Registration: 15 16 def __init__(self, image:np.ndarray, ref_image:np.ndarray): 17 """ 18 Register an image against a reference image 19 :param image: 2D array to register 20 :param ref_image: 2D array to use as reference 21 """ 22 23 assert image.ndim == 2, "Image must be 2D" 24 assert ref_image.ndim == 2, "Image must be 2D" 25 26 self.image = make_even_square(image[np.newaxis,:,:].astype(np.float32))[0,:,:] 27 self.ref_image = make_even_square(ref_image[np.newaxis,:,:].astype(np.float32))[0,:,:] 28 29 self.original_dtype = image.dtype 30 31 self.w = self.image.shape[1] 32 self.h = self.image.shape[0] 33 self.wref = self.ref_image.shape[1] 34 self.href = self.ref_image.shape[0] 35 36 self.reg_result = {} 37 38 39 def translation(self): 40 """ 41 Registers the images considering only translation 42 :return: registered image 43 """ 44 45 shifts, max_sim = self.phase_correlation(self.ref_image, self.image) 46 47 # The size of the CCM array is the same as the image 48 y_shift = (self.h/2.0 - shifts[0]) 49 x_shift = (self.w/2.0 - shifts[1]) 50 51 translated = Interpolator(self.image).shift(x_shift,y_shift).astype(self.original_dtype) 52 53 self.reg_result = {'Image':translated, 'Translation':(y_shift,x_shift), 'Scaling':None, 'Rotation':None, 'Max_Sim':max_sim} 54 55 return translated 56 57 58 def scaled_rotation(self): 59 """ 60 Registers the images considering only rotation and isotropic scaling 61 """ 62 63 lpolar_image = Interpolator(self.image).polar(scale='log') 64 lpolar_ref_image = Interpolator(self.ref_image).polar(scale='log') 65 shifts, max_sim = self.phase_correlation(lpolar_ref_image, lpolar_image) 66 67 # Size of the polar transform is always (360,maxradius) 68 h = 360 69 w = np.hypot(self.w/2, self.h/2) 70 71 angle = -np.deg2rad((h/2 - shifts[0])) 72 log_translation = (w/2-shifts[1]) * np.log(w) / w 73 scale = np.exp(log_translation) 74 75 scaled = Interpolator(self.image).scale_xy(scale, scale) 76 rotated = Interpolator(scaled).rotate(angle).astype(self.original_dtype) 77 78 self.reg_result = {'Image':rotated, 'Translation':None, 'Scaling':scaled, 'Rotation':angle, 'Max_Sim':max_sim} 79 80 return rotated 81 82 def scaling_rotation_translation(self): 83 """ 84 Registers the images considering rotation, isotropic scaling and translation 85 Based upon: 86 An FFT-Based Technique for Translation,Rotation, 87 and Scale-Invariant Image Registration 88 B. Srinivasa Reddy and B. N. Chatterji 89 """ 90 # Step 0: Prepare some heavily used vars 91 h = 360 92 w = np.hypot(self.w/2, self.h/2) 93 highpass_filter = self.highpass_filter((self.h,self.w)) 94 95 # Step 1: Prep the reference image for iteration 96 windowed_ref_image = self.ref_image * window('hann', self.ref_image.shape) 97 freq_ref_image = np.abs(np.fft.fftshift(np.fft.fft2(windowed_ref_image)) * highpass_filter).astype(np.float32) 98 lpolar_ref_image = Interpolator(freq_ref_image).polar('log') 99 100 # Step 2: Iterate to find scale and angle 101 total_angle = 0 102 total_scale = 1 103 iter_image = self.image 104 105 for iter in range(10): 106 windowed_image = iter_image * window('hann', iter_image.shape) 107 f_image = np.abs(np.fft.fftshift(np.fft.fft2(windowed_image))*highpass_filter).astype(np.float32) 108 lpolar_image = Interpolator(f_image).polar('log') 109 110 shifts, max_sim_1 = self.phase_correlation(lpolar_ref_image, lpolar_image) 111 112 angle = -np.deg2rad((h/2 - shifts[0])) 113 log_translation = (w/2-shifts[1]) * np.log(w) / w 114 scale = np.exp(-1*log_translation) # NEGATIVE SIGN HERE IS MANDATORY 115 116 total_angle += angle 117 total_scale *= scale 118 119 scaled = Interpolator(self.image).scale_xy(total_scale, total_scale) 120 rotated = Interpolator(scaled).rotate(total_angle) 121 iter_image = rotated 122 123 # Step 3: Find translation AND the correct angle 124 # As outlined in the ref, in the frequency space 180-angle or angle have the same effect so there is ambiguity 125 # We test for both options and choose the one with the highest peak in the ccm 126 127 final_scaled_img = Interpolator(self.image).scale_xy(total_scale, total_scale) 128 129 final_option_1 = Interpolator(final_scaled_img).rotate(total_angle) 130 final_option_2 = Interpolator(final_scaled_img).rotate(np.pi+total_angle) 131 132 shifts_1, sim_1 = self.phase_correlation(self.ref_image, final_option_1) 133 shifts_2, sim_2 = self.phase_correlation(self.ref_image, final_option_2) 134 135 if sim_1 > sim_2: 136 final_iter_image = final_option_1 137 shifts = shifts_1 138 max_sim_2 = sim_1 139 else: 140 final_iter_image = final_option_2 141 shifts = shifts_2 142 max_sim_2 = sim_2 143 144 y_shift = (self.h/2.0 - shifts[0]) 145 x_shift = (self.w/2.0 - shifts[1]) 146 147 translated = Interpolator(final_iter_image).shift(x_shift,y_shift).astype(self.original_dtype) 148 149 self.reg_result = {'Image':translated, 'Translation':(y_shift,x_shift), 'Scaling':total_scale, 'Rotation':total_angle, 'Max_Sim':(max_sim_1,max_sim_2)} 150 151 return translated 152 153 @staticmethod 154 def phase_correlation(im1:np.ndarray, im2:np.ndarray)->tuple: 155 """ 156 Perform phase correlation between two images and return the shift that maximizes the overlap between the images 157 :param im1: 2D array of np.float32 158 :param im2: 2D array of np.float32 159 :return: coordinate tuple of the maximum point of the ccm and max value of the ccm 160 """ 161 162 ccm = calculate_slice_ccm(im1, im2) 163 optimizer = GetMaxOptimizer(ccm) 164 shifts = optimizer.get_max() 165 maxsim = -optimizer.get_interpolated_px_value(shifts) 166 167 return shifts, maxsim 168 169 @staticmethod 170 @cache 171 def highpass_filter(shape)->np.ndarray: 172 173 n_row = shape[0] 174 n_col = shape[1] 175 row_freq_arr = np.fft.fftshift(np.fft.fftfreq(n_row)) 176 col_freq_arr = np.fft.fftshift(np.fft.fftfreq(n_col)) 177 row_f,col_f = np.meshgrid(row_freq_arr, col_freq_arr, indexing='ij') 178 X = np.cos(np.pi*row_f) * np.cos(np.pi*col_f) 179 H = (1-X)*(2-X) 180 181 return H.astype(np.float32)
Registration(image: numpy.ndarray, ref_image: numpy.ndarray)
16 def __init__(self, image:np.ndarray, ref_image:np.ndarray): 17 """ 18 Register an image against a reference image 19 :param image: 2D array to register 20 :param ref_image: 2D array to use as reference 21 """ 22 23 assert image.ndim == 2, "Image must be 2D" 24 assert ref_image.ndim == 2, "Image must be 2D" 25 26 self.image = make_even_square(image[np.newaxis,:,:].astype(np.float32))[0,:,:] 27 self.ref_image = make_even_square(ref_image[np.newaxis,:,:].astype(np.float32))[0,:,:] 28 29 self.original_dtype = image.dtype 30 31 self.w = self.image.shape[1] 32 self.h = self.image.shape[0] 33 self.wref = self.ref_image.shape[1] 34 self.href = self.ref_image.shape[0] 35 36 self.reg_result = {}
Register an image against a reference image
Parameters
- image: 2D array to register
- ref_image: 2D array to use as reference
def
translation(self):
39 def translation(self): 40 """ 41 Registers the images considering only translation 42 :return: registered image 43 """ 44 45 shifts, max_sim = self.phase_correlation(self.ref_image, self.image) 46 47 # The size of the CCM array is the same as the image 48 y_shift = (self.h/2.0 - shifts[0]) 49 x_shift = (self.w/2.0 - shifts[1]) 50 51 translated = Interpolator(self.image).shift(x_shift,y_shift).astype(self.original_dtype) 52 53 self.reg_result = {'Image':translated, 'Translation':(y_shift,x_shift), 'Scaling':None, 'Rotation':None, 'Max_Sim':max_sim} 54 55 return translated
Registers the images considering only translation
Returns
registered image
def
scaled_rotation(self):
58 def scaled_rotation(self): 59 """ 60 Registers the images considering only rotation and isotropic scaling 61 """ 62 63 lpolar_image = Interpolator(self.image).polar(scale='log') 64 lpolar_ref_image = Interpolator(self.ref_image).polar(scale='log') 65 shifts, max_sim = self.phase_correlation(lpolar_ref_image, lpolar_image) 66 67 # Size of the polar transform is always (360,maxradius) 68 h = 360 69 w = np.hypot(self.w/2, self.h/2) 70 71 angle = -np.deg2rad((h/2 - shifts[0])) 72 log_translation = (w/2-shifts[1]) * np.log(w) / w 73 scale = np.exp(log_translation) 74 75 scaled = Interpolator(self.image).scale_xy(scale, scale) 76 rotated = Interpolator(scaled).rotate(angle).astype(self.original_dtype) 77 78 self.reg_result = {'Image':rotated, 'Translation':None, 'Scaling':scaled, 'Rotation':angle, 'Max_Sim':max_sim} 79 80 return rotated
Registers the images considering only rotation and isotropic scaling
def
scaling_rotation_translation(self):
82 def scaling_rotation_translation(self): 83 """ 84 Registers the images considering rotation, isotropic scaling and translation 85 Based upon: 86 An FFT-Based Technique for Translation,Rotation, 87 and Scale-Invariant Image Registration 88 B. Srinivasa Reddy and B. N. Chatterji 89 """ 90 # Step 0: Prepare some heavily used vars 91 h = 360 92 w = np.hypot(self.w/2, self.h/2) 93 highpass_filter = self.highpass_filter((self.h,self.w)) 94 95 # Step 1: Prep the reference image for iteration 96 windowed_ref_image = self.ref_image * window('hann', self.ref_image.shape) 97 freq_ref_image = np.abs(np.fft.fftshift(np.fft.fft2(windowed_ref_image)) * highpass_filter).astype(np.float32) 98 lpolar_ref_image = Interpolator(freq_ref_image).polar('log') 99 100 # Step 2: Iterate to find scale and angle 101 total_angle = 0 102 total_scale = 1 103 iter_image = self.image 104 105 for iter in range(10): 106 windowed_image = iter_image * window('hann', iter_image.shape) 107 f_image = np.abs(np.fft.fftshift(np.fft.fft2(windowed_image))*highpass_filter).astype(np.float32) 108 lpolar_image = Interpolator(f_image).polar('log') 109 110 shifts, max_sim_1 = self.phase_correlation(lpolar_ref_image, lpolar_image) 111 112 angle = -np.deg2rad((h/2 - shifts[0])) 113 log_translation = (w/2-shifts[1]) * np.log(w) / w 114 scale = np.exp(-1*log_translation) # NEGATIVE SIGN HERE IS MANDATORY 115 116 total_angle += angle 117 total_scale *= scale 118 119 scaled = Interpolator(self.image).scale_xy(total_scale, total_scale) 120 rotated = Interpolator(scaled).rotate(total_angle) 121 iter_image = rotated 122 123 # Step 3: Find translation AND the correct angle 124 # As outlined in the ref, in the frequency space 180-angle or angle have the same effect so there is ambiguity 125 # We test for both options and choose the one with the highest peak in the ccm 126 127 final_scaled_img = Interpolator(self.image).scale_xy(total_scale, total_scale) 128 129 final_option_1 = Interpolator(final_scaled_img).rotate(total_angle) 130 final_option_2 = Interpolator(final_scaled_img).rotate(np.pi+total_angle) 131 132 shifts_1, sim_1 = self.phase_correlation(self.ref_image, final_option_1) 133 shifts_2, sim_2 = self.phase_correlation(self.ref_image, final_option_2) 134 135 if sim_1 > sim_2: 136 final_iter_image = final_option_1 137 shifts = shifts_1 138 max_sim_2 = sim_1 139 else: 140 final_iter_image = final_option_2 141 shifts = shifts_2 142 max_sim_2 = sim_2 143 144 y_shift = (self.h/2.0 - shifts[0]) 145 x_shift = (self.w/2.0 - shifts[1]) 146 147 translated = Interpolator(final_iter_image).shift(x_shift,y_shift).astype(self.original_dtype) 148 149 self.reg_result = {'Image':translated, 'Translation':(y_shift,x_shift), 'Scaling':total_scale, 'Rotation':total_angle, 'Max_Sim':(max_sim_1,max_sim_2)} 150 151 return translated
Registers the images considering rotation, isotropic scaling and translation Based upon: An FFT-Based Technique for Translation,Rotation, and Scale-Invariant Image Registration B. Srinivasa Reddy and B. N. Chatterji
@staticmethod
def
phase_correlation(im1: numpy.ndarray, im2: numpy.ndarray) -> tuple:
153 @staticmethod 154 def phase_correlation(im1:np.ndarray, im2:np.ndarray)->tuple: 155 """ 156 Perform phase correlation between two images and return the shift that maximizes the overlap between the images 157 :param im1: 2D array of np.float32 158 :param im2: 2D array of np.float32 159 :return: coordinate tuple of the maximum point of the ccm and max value of the ccm 160 """ 161 162 ccm = calculate_slice_ccm(im1, im2) 163 optimizer = GetMaxOptimizer(ccm) 164 shifts = optimizer.get_max() 165 maxsim = -optimizer.get_interpolated_px_value(shifts) 166 167 return shifts, maxsim
Perform phase correlation between two images and return the shift that maximizes the overlap between the images
Parameters
- im1: 2D array of np.float32
- im2: 2D array of np.float32
Returns
coordinate tuple of the maximum point of the ccm and max value of the ccm
@staticmethod
@cache
def
highpass_filter(shape) -> numpy.ndarray:
169 @staticmethod 170 @cache 171 def highpass_filter(shape)->np.ndarray: 172 173 n_row = shape[0] 174 n_col = shape[1] 175 row_freq_arr = np.fft.fftshift(np.fft.fftfreq(n_row)) 176 col_freq_arr = np.fft.fftshift(np.fft.fftfreq(n_col)) 177 row_f,col_f = np.meshgrid(row_freq_arr, col_freq_arr, indexing='ij') 178 X = np.cos(np.pi*row_f) * np.cos(np.pi*col_f) 179 H = (1-X)*(2-X) 180 181 return H.astype(np.float32)